﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using DeepEarth.Core;
using DeepEarth.Core.Controls;
using DeepEarth.Core.Data;
using DeepEarth.Core.Utilities;
using DeepEarth.Map.Core.Clustering;
using DeepEarth.Map.Core.Converters;
using DeepEarth.Map.Core.Utilities;

#if !WINDOWS_PHONE
using System.Reactive.Linq;
#else
using Microsoft.Phone.Reactive;
#endif

namespace DeepEarth.Map.Core
{
    /// <summary>
    /// Manages a set of panels used to add elements to the visual tree through databinding.
    /// </summary>
    public class LayerGroup : ILayer, INotifyPropertyChanged
    {        
        ObservableCollection<LayerGroup> ChildLayers = new ObservableCollection<LayerGroup>();

        VectorLayerCollection vectorLayerContainer;
        Canvas dynamicLayerContainer;
        Canvas labelLayerContainer;
        RasterOverlayCollection rasterOverlayCollection;
        
        RasterOverlayCollection childRasterOverlayCollection;
        RasterOverlayCollection ChildRasterOverlayCollection
        {
            get
            {
                if (childRasterOverlayCollection == null)
                {
                    childRasterOverlayCollection = new RasterOverlayCollection();
                    this.rasterOverlayCollection.Children.Add(childRasterOverlayCollection);
                    ApplyZOrderBinding(childRasterOverlayCollection);
                    ApplyOpacityBinding(childRasterOverlayCollection);
                }
                return childRasterOverlayCollection;
            }
        }


        public DynamicLayer DynamicLayer { get; set; }
        public LabelLayer LabelLayer { get; set; }
        public VectorLayer VectorLayer { get; set; }

        public bool UseLabelLayer { get; set; }

        public MultiScaleImage RasterOverlay { get; set; }
        public MultiScaleImage RasterLabelOverlay { get; set; }

        public TemplateSelector TemplateSelector { get; set; }
        public TemplateSelector LabelTemplateSelector { get; set; }

        readonly Dictionary<GeoItemViewModel, UIElement> dynamicLayerContents = new Dictionary<GeoItemViewModel, UIElement>();
        readonly Dictionary<GeoItemViewModel, UIElement> labelLayerContents = new Dictionary<GeoItemViewModel, UIElement>();
        readonly Dictionary<GeoItemViewModel, UIElement> vectorLayerContents = new Dictionary<GeoItemViewModel, UIElement>();

        // typically this will be the same as the dynamic layer contents
        readonly Dictionary<ClusterViewModel, UIElement> elementsToSynchronize = new Dictionary<ClusterViewModel, UIElement>();

        IMap map;
        public IMap MapInstance
        {
            get { return map; }
            set
            {
                map = value;
                var weakChangeOnFrame = new WeakEventListener<LayerGroup, IMap>(this, map)
                {
                    DetatchAction = (listener, source) => { source.ViewChangeOnFrame -= listener.OnEvent; },
                    EventAction = (self, sender, args) => { self.MapInstance_ViewChangeOnFrame(sender, args); }
                };
                map.ViewChangeOnFrame += weakChangeOnFrame.OnEvent;

                var weakChangeEnd = new WeakEventListener<LayerGroup, IMap>(this, map)
                {
                    DetatchAction = (listener, source) => { source.ViewChangeEnd -= listener.OnEvent; },
                    EventAction = (self, sender, args) => { self.MapInstance_ViewChangeOnFrame(sender, args); }
                };
                map.ViewChangeEnd += weakChangeEnd.OnEvent;

                dispatcher = map.AsFrameworkElement.Dispatcher;
            }
        }      

        void MapInstance_ViewChangeOnFrame(object sender, EventArgs e)
        {
            Synchronize();
        }

        // Sometimes applying a template to a view model will result in the creation of a new layer.
        // if so, associate that view Model with the layer.
        GeoItemViewModel viewModel;
        public GeoItemViewModel ViewModel
        {
            get { return viewModel; }
            set
            {
                if (viewModel != null)
                {
                    if (RasterOverlay != null)
                    {
                        rasterOverlayCollection.Children.Remove(RasterOverlay);
                        RasterOverlay = null;
                    }
                    if (RasterLabelOverlay != null)
                    {
                        labelLayerContainer.Children.Remove(RasterLabelOverlay);
                        RasterLabelOverlay = null;
                    }
                }
                viewModel = value;

                // the viewmodel might be null if this is the root layer
                if (viewModel != null)
                {
                    ItemsSource = viewModel.Children;
                    var rasterable = viewModel.Data as IRasterable;

                    if (rasterable != null)
                    {
                        InitializeRasterOverlay(rasterable);
                        rasterable.SignalRefresh += OnRasterRefresh;
                    }

                    var weakEvent = new WeakEventListener<LayerGroup, GeoItemViewModel>(this, viewModel)
                    {
                        DetatchAction = (listener, source) => source.PropertyChanged -= listener.OnEvent,
                        EventAction = (self, sender, args) => self.viewModel_PropertyChanged(sender, (PropertyChangedEventArgs)args)
                    };
                    viewModel.PropertyChanged += weakEvent.OnEvent;
                }

                OnViewModelActiveChanged();
            }
        }

         public LayerGroup(IMap map, Canvas dynamicLayerContainer, VectorLayerCollection vectorLayerContainer, RasterOverlayCollection rasterOverlayCollection, Canvas labelLayerContainer)
        {
            this.dynamicLayerContainer = dynamicLayerContainer;
            this.vectorLayerContainer = vectorLayerContainer;
            this.labelLayerContainer = labelLayerContainer;
            this.rasterOverlayCollection = rasterOverlayCollection;

            MapInstance = map;

            DynamicLayer = new DynamicLayer();
            dynamicLayerContainer.Children.Add(DynamicLayer);
            ApplyZOrderBinding(DynamicLayer);
            ApplyOpacityBinding(DynamicLayer);

            VectorLayer = new VectorLayer();
            vectorLayerContainer.Children.Add(VectorLayer);
            ApplyZOrderBinding(VectorLayer);
            ApplyOpacityBinding(VectorLayer);

            LabelLayer = new LabelLayer();
            labelLayerContainer.Children.Add(LabelLayer);
            ApplyZOrderBinding(LabelLayer);
            ApplyOpacityBinding(LabelLayer);

            filterProcessor = new FilterProcessor(DynamicLayer.LocationToViewportPoint, filterCollectionLock);

            ChildLayers.CollectionChanged += (o, e) => ResetChildLayerZOrdering();

            InitializeAsyncAddSystem();
        }

        //Constructor called by purely child layers i.e. layers that should share staticlayer and dynamic layer instances
        LayerGroup(IMap map, DynamicLayer dynamicLayer, VectorLayer vectorLayer, LabelLayer labelLayer, RasterOverlayCollection rasterOverlayCollection, Canvas labelLayerContainer)
        {
            this.rasterOverlayCollection = rasterOverlayCollection;
            this.labelLayerContainer = labelLayerContainer;

            MapInstance = map;
            DynamicLayer = dynamicLayer;
            LabelLayer = labelLayer;
            VectorLayer = vectorLayer;

            filterProcessor = new FilterProcessor(DynamicLayer.LocationToViewportPoint, filterCollectionLock);

            ChildLayers.CollectionChanged += (o, e) => ResetChildLayerZOrdering();

            InitializeAsyncAddSystem();
        }

        void OnRasterRefresh(MultiScaleTileSource msts)
        {
            var rasterable = ViewModel.Data as IRasterable;
            if (RasterOverlay == null)
            {
                if (rasterable != null)
                {
                    InitializeRasterOverlay(rasterable);
                }
            }
            else
            {
                RasterCrossoverViewportWidth = rasterable.RasterCrossoverViewportWidth;
                RasterOverlay.Dispatcher.BeginInvoke(() => RasterOverlay.Source = msts );
                if (rasterable.LabelsTileSource != null)
                    RasterLabelOverlay.Dispatcher.BeginInvoke(() => RasterLabelOverlay.Source = rasterable.LabelsTileSource);
            }
        }

        void InitializeRasterOverlay(IRasterable rasterable)
        {
            if (rasterable.RasterTileSource != null)
            {
                RasterOverlay = new MultiScaleImage { IsHitTestVisible = false, Source = rasterable.RasterTileSource };


#if !WINDOWS_PHONE
                Observable.FromEventPattern<EventArgs>(MapInstance, "ViewChangeEnd")
                    .Throttle(TimeSpan.FromMilliseconds(1000))
                    .Subscribe(e => SignalThrottledViewChange());

                var weakChangeOnFrame = new WeakEventListener<LayerGroup, IMap>(this, map)
                {
                    DetatchAction = (listener, source) => { source.ViewChangeOnFrame -= listener.OnEvent; },
                    EventAction = (self, sender, args) => { self.OnViewChange(sender, args); }
                };
                map.ViewChangeOnFrame += weakChangeOnFrame.OnEvent;

                SetMinZoomLevelThreshholds();

#else
                Observable.FromEvent<EventArgs>(MapInstance, "ViewChangeEnd")
                    .Throttle(TimeSpan.FromMilliseconds(1000))
                    .Subscribe(e => SignalViewChange());
#endif

                RasterCrossoverViewportWidth = rasterable.RasterCrossoverViewportWidth;
                rasterOverlayCollection.Children.Add(RasterOverlay);
                ApplyZOrderBinding(RasterOverlay);
                ApplyOpacityBinding(RasterOverlay);
            }

            if (rasterable.LabelsTileSource != null && labelLayerContainer != null)
            {
                RasterLabelOverlay = new MultiScaleImage { IsHitTestVisible = false, Source = rasterable.LabelsTileSource, UseSprings = false };
                labelLayerContainer.Children.Add(RasterLabelOverlay);
                ApplyZOrderBinding(RasterLabelOverlay);
                ApplyOpacityBinding(RasterLabelOverlay);
            }

            if (MapInstance != null && (rasterable.RasterTileSource != null || rasterable.LabelsTileSource != null))
            {
                SynchronizeLayers();
            }
        }

        void SignalThrottledViewChange()
        {
            if (IsVisible && RasterOverlay != null)
            {
                var rasterable = ViewModel.Data as IRasterable;

                if (rasterable != null)
                {
                    rasterable.ViewChanged(RasterOverlay);
                }
            }
        }

        void OnViewChange(object sender, EventArgs e)
        {
            SetMinZoomLevelThreshholds();
        }

        void SetMinZoomLevelThreshholds()
        {
            if (RasterOverlay != null)
            {
                var rasterable = ViewModel.Data as IRasterable;

                if (rasterable != null)
                {
                    if (rasterable.RasterTileSource != null)
                    {
                        rasterable.RasterTileSource.OnZoomLevelChanged(map.ZoomLevel);
                    }
                    if (rasterable.LabelsTileSource != null)
                    {
                        rasterable.LabelsTileSource.OnZoomLevelChanged(map.ZoomLevel);
                    }
                }
            }
        }

        void viewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "IsActive")
            {
                OnViewModelActiveChanged();

                if (ViewModel.IsActive)
                {
                    // might need to trigger a refresh of the label layer
                    OnViewModelShowDetailsChanged();
                }
            }

            // Note: This just triggers a refresh. The visibility of each label is bound 
            // to the ShowDetails property of the view model.
            if (e.PropertyName == "ShowDetails")
            {
                OnViewModelShowDetailsChanged();
            }
        }

        double RasterCrossoverViewportWidth { get; set; }

        FilterProcessor filterProcessor;

        BooleanVisibilityConverter bvc;
        BooleanVisibilityConverter VisibilityConverter
        {
            get
            {
                if (bvc == null)
                    bvc = new BooleanVisibilityConverter();
                return bvc;
            }
        }

        LabelVisibilityConverter lvc;
        LabelVisibilityConverter LabelVisibilityConverter
        {
            get
            {
                if (lvc == null)
                    lvc = new LabelVisibilityConverter(this);
                return lvc;
            }
        }

        // the ElementsToSynchronize collection is passed to the filterprocessor, which queries it. If it attempts to query it while 
        // it is modified by the main thread, then an exception will be thrown. Use this shared lock to prevent this from happening.
        object filterCollectionLock = new object();

        // If the dynamicLayerContainer, staticLayerContainer are null for this instance, then it is using shared layers.
        // If not null, the layer has its own static, dynamic and label layers. This has implications for cleanup
        // Label layer container also operates as a rasterLabelOverlayCollection so it should never be null.
        bool UsingSharedLayers()
        {
            return vectorLayerContainer == null && dynamicLayerContainer == null;
        }

        WeakEventListener<LayerGroup, ObservableCollection<GeoItemViewModel>> collectionChangeListener;

        ObservableCollection<GeoItemViewModel> itemsSource;
        public ObservableCollection<GeoItemViewModel> ItemsSource
        {
            get { return itemsSource; }
            set
            {
                DetatchFromItemsSource();

                itemsSource = value;

                ClearItems();

                // if there are already child items, they will be templated and potentially added to the static and dynamic layers
                // Ensure that the Clear() calls occur above and not below
                int index = 0;
                foreach (var vm in value)
                {
                    itemsSource_CollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, vm, index));
                    index++;
                }
                //itemsSource.CollectionChanged += itemsSource_CollectionChanged;

                collectionChangeListener = null;
                collectionChangeListener = new WeakEventListener<LayerGroup, ObservableCollection<GeoItemViewModel>>(this, itemsSource)
                                               {
                                                   DetatchAction = (listener, source) => { source.CollectionChanged -= listener.OnEvent; }
                                               };


                itemsSource.CollectionChanged += collectionChangeListener.OnEvent;

                collectionChangeListener.EventAction = (self, source, eventArgs) => self.itemsSource_CollectionChanged((NotifyCollectionChangedEventArgs)eventArgs);
            }
        }

        void DetatchFromItemsSource()
        {
            if (itemsSource != null && collectionChangeListener != null)
            {
                collectionChangeListener.DetatchAction(collectionChangeListener, itemsSource);
            }
        }

        public void Clear()
        {
            DetatchFromItemsSource();

            var rasterable = viewModel.Data as IRasterable;

            if (rasterable != null)
            {
                rasterable.SignalRefresh -= OnRasterRefresh;
            }

            if (rasterOverlayCollection != null && RasterOverlay != null)
            {
                rasterOverlayCollection.Children.Remove(RasterOverlay);
                RasterOverlay = null;
            }

            if (rasterOverlayCollection != null && childRasterOverlayCollection != null)
            {
                rasterOverlayCollection.Children.Remove(childRasterOverlayCollection);
                childRasterOverlayCollection.Children.Clear();
                childRasterOverlayCollection = null;
            }

            if (labelLayerContainer != null && RasterLabelOverlay != null)
            {
                labelLayerContainer.Children.Remove(RasterLabelOverlay);
                RasterLabelOverlay = null;
            }

            if (!UsingSharedLayers())
            {
                dynamicLayerContainer.Children.Remove(DynamicLayer);
                labelLayerContainer.Children.Remove(LabelLayer);
                vectorLayerContainer.Children.Remove(VectorLayer);
            }

            ClearItems();
        }

        void ClearItems()
        {
            lock (synLock)
            {
                pendingAdds.Clear();
            }

            if (DynamicLayer != null)
            {
                foreach (var kvp in dynamicLayerContents)
                {
                    DynamicLayer.Remove(kvp.Key, kvp.Value);
                }

                dynamicLayerContents.Clear();
            }

            if (VectorLayer != null)
            {
                foreach (var kvp in vectorLayerContents)
                {
                    VectorLayer.Remove(kvp.Key, kvp.Value);
                }
                vectorLayerContents.Clear();
            }

            lock (filterCollectionLock)
            {
                elementsToSynchronize.Clear();
            }

            foreach (var kvp in labelLayerContents)
            {
                LabelLayer.Remove(kvp.Key, kvp.Value);
            }

            labelLayerContents.Clear();

            foreach (var c in ChildLayers)
            {
                c.Clear();
            }
            ChildLayers.Clear();
        }

        #region async element add

        object synLock = new object();
        ObservableCollection<GeoItemViewModel> pendingAdds;
        int chunkSize = 10;
        const int ChunkUnit = 10;
        const int PauseInMilliseconds = 0;
        const double ChunkTimeThresholdInMilliseconds = 100.0;
        bool isProcessing;
        Dispatcher dispatcher;

        void InitializeAsyncAddSystem()
        {
            pendingAdds = new ObservableCollection<GeoItemViewModel>();

#if !WINDOWS_PHONE
        Observable.FromEventPattern<NotifyCollectionChangedEventArgs>(pendingAdds, "CollectionChanged")
                .Throttle(TimeSpan.FromMilliseconds(100))
                .Subscribe(e => pendingAdds_CollectionChanged(e.EventArgs));
#else
        Observable.FromEvent<NotifyCollectionChangedEventArgs>(pendingAdds, "CollectionChanged")
                .Throttle(TimeSpan.FromMilliseconds(100))
                .Subscribe(e => pendingAdds_CollectionChanged(e.EventArgs));
#endif
        }

        void pendingAdds_CollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            if (e.Action != NotifyCollectionChangedAction.Add)
            {
                return;
            }

            // Ensure one thread
            // lock isProcessing
            lock (synLock)
            {
                if (isProcessing)
                {
                    return;
                }
                isProcessing = true;
            }

            // need this redirect because AddPending clears the collection so we can't do this on the same thread.
            dispatcher.BeginInvoke(AsyncAddPending);
        }

        void AsyncAddPending()
        {
            // lock PendingAdds
            var intermediate = new List<GeoItemViewModel>();
            lock (synLock)
            {
                int index = 0;
                while (index < chunkSize && pendingAdds.Count > 0)
                {
                    intermediate.Add(pendingAdds[0]);
                    pendingAdds.RemoveAt(0);
                    index++;
                }
            }
            
            if (intermediate.Count > 0)
            {
                var startTime = DateTime.UtcNow;
                foreach (var item in intermediate)
                {
                    ApplyTemplate(item);
                }

                var endTime = DateTime.UtcNow;

                var elapsed = (endTime - startTime).TotalMilliseconds;

                if (elapsed > ChunkTimeThresholdInMilliseconds)
                {
                    chunkSize = Math.Max(ChunkUnit, chunkSize - ChunkUnit);
                }
                else
                {
                    chunkSize += ChunkUnit;
                }

                System.Threading.ThreadPool.QueueUserWorkItem(state =>
                                                                  {
                                                                      System.Threading.Thread.Sleep(PauseInMilliseconds);
                                                                      dispatcher.BeginInvoke(AsyncAddPending);
                                                                  });
            }
            else
            {
                isProcessing = false;
            }        
        }

        #endregion

        void itemsSource_CollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Reset:
                    {
                        ClearItems();
                        break;
                    }

                case NotifyCollectionChangedAction.Add:

                    lock (synLock)
                    {
                        foreach (var item in e.NewItems)
                        {
                            var vm = (GeoItemViewModel) item;

                            if (!vm.IsPlaceHolder)
                            {
                                pendingAdds.Add(vm);
                            }
                        }
                    }
                    break;

                case NotifyCollectionChangedAction.Remove:
                    foreach (GeoItemViewModel vm in e.OldItems)
                    {
                        var cvm = dynamicLayerContents.Keys.OfType<ClusterViewModel>().Where(k => k.WrappedViewModel == vm).FirstOrDefault();
                        if (cvm != null)
                        {
                            DynamicLayer.Remove(cvm, dynamicLayerContents[cvm]);
                            dynamicLayerContents.Remove(cvm);
                        }
                        else
                        {
                            cvm = vectorLayerContents.Keys.OfType<ClusterViewModel>().Where(k => k.WrappedViewModel == vm).FirstOrDefault();
                            if (cvm != null)
                            {
                                VectorLayer.Remove(cvm, vectorLayerContents[cvm]);
                                vectorLayerContents.Remove(cvm);
                            }
                        }
                        
                        if (dynamicLayerContents.ContainsKey(vm))
                        {
                            DynamicLayer.Remove(vm, dynamicLayerContents[vm]);
                            dynamicLayerContents.Remove(vm);
                        }
                        else if (vectorLayerContents.ContainsKey(vm))
                        {
                            VectorLayer.Remove(cvm, vectorLayerContents[cvm]);
                            vectorLayerContents.Remove(cvm);
                        }

                        var clusterViewModel = elementsToSynchronize.Keys.Where(k => k.WrappedViewModel == vm).FirstOrDefault();

                        if (clusterViewModel != null)
                        {
                            lock (filterCollectionLock)
                            {
                                elementsToSynchronize.Remove(clusterViewModel);
                            }
                        }

                        var childLayer = ChildLayers.Where(l => l.viewModel == vm).FirstOrDefault();

                        if (childLayer != null)
                        {
                            childLayer.Clear();
                            ChildLayers.Remove(childLayer);
                        }
                    }
                    break;
            }
        }

        void ApplyTemplate(GeoItemViewModel vm)
        {
            DataTemplate t = null;
            
            if (TemplateSelector != null)
                t = TemplateSelector.FindTemplate<DataTemplate>(vm.Data);

            if (t == null && vm.Data is UIElement)
            {
                var element = vm.Data as UIElement;
                ApplyAdorner(vm, element);
            }

            if (t == null && (vm.Data is IEnumerable || vm.Data is IItemsContainer || vm.Data is IRasterable))
            {
                LayerGroup layer;

                if (vm.AtomicParent == null || vm.AtomicParent == vm)
                {
                    layer = new LayerGroup(MapInstance, dynamicLayerContainer, vectorLayerContainer, ChildRasterOverlayCollection, labelLayerContainer);
                }
                else
                    layer = new LayerGroup(MapInstance, DynamicLayer, VectorLayer, LabelLayer, ChildRasterOverlayCollection, labelLayerContainer);

                if (vm.Data is IItemsContainer)
                {
                    layer.ApplyOpacityBinding((IItemsContainer)vm.Data);
                }
                else if (vm.Data is IRasterable)
                {
                    layer.ApplyOpacityBinding((IRasterable)vm.Data);
                }
                
                layer.UseLabelLayer = UseLabelLayer;
                layer.TemplateSelector = TemplateSelector;
                layer.LabelTemplateSelector = LabelTemplateSelector;
                layer.ViewModel = vm;
                layer.ParentLayer = this;
                layer.ZOrder = ZOrder;
                ChildLayers.Add(layer);
            }

            if (t != null)
            {
                var fe = MapHelper.CreateElement(t, vm.Data);
                
                Location elementPosition = vm.Position;
                if (elementPosition == null)
                {
                    if (fe is ILocatable)
                    {
                        ILocatable locatable = fe as ILocatable;
                        vm.Position = locatable.Position;
                    }

                    if (fe is IMultiLocation)
                    {
                        IMultiLocation multiLoc = fe as IMultiLocation;
                        vm.Position = multiLoc.Position;

                        if (vm.Locations == null)
                        {
                            vm.Locations = multiLoc.Locations;
                        }
                    }
                }

                if (fe != null)
                    ApplyAdorner(vm, fe);

#if !WINDOWS_PHONE
                // if t is a hierarchical data template, this is a hint that the 
                // model (viewmodel.Data) is hierarchical. If so, update the viewModel's child
                // item source so that it will build up its collection of child viewmodels.
                if (t is HierarchicalDataTemplate)
                {
                    var ht = t as HierarchicalDataTemplate;
                    if (ht.ItemsSource != null)
                    {
                        PropertyPath pp = ht.ItemsSource.Path;

                        Type itemType = vm.Data.GetType();
                        PropertyInfo pi = itemType.GetProperty(pp.Path);

                        var enumerable = pi.GetValue(vm.Data, null) as IEnumerable;

                        if (enumerable != null && vm.ChildItemsSource == null)
                        {
                            vm.ChildItemsSource = enumerable;
                        }

                        LayerGroup layer;
                        if (dynamicLayerContainer != null && (vm.AtomicParent == null || vm.AtomicParent == vm))
                        {                            
                            layer = new LayerGroup(MapInstance, dynamicLayerContainer, vectorLayerContainer, ChildRasterOverlayCollection, labelLayerContainer);
                        }
                        else
                        {
                            layer = new LayerGroup(MapInstance, DynamicLayer, VectorLayer, LabelLayer, ChildRasterOverlayCollection, labelLayerContainer);
                        }

                        if (vm.Data is IItemsContainer)
                        {
                            layer.ApplyOpacityBinding((IItemsContainer)vm.Data);
                        }
                        else if (vm.Data is IRasterable)
                        {
                            layer.ApplyOpacityBinding((IRasterable)vm.Data);
                        }

                        layer.UseLabelLayer = UseLabelLayer;
                        layer.TemplateSelector = TemplateSelector;
                        layer.LabelTemplateSelector = LabelTemplateSelector;
                        layer.ParentLayer = this;
                        layer.ViewModel = vm;
                        layer.ZOrder = ZOrder;
                        ChildLayers.Add(layer);
                    }
                }
#endif
            }
        }

        void ApplyAdorner(GeoItemViewModel vm, UIElement itemElement)
        {
            var adorner = new MapItemAdorner();
            var clusterViewModel = new ClusterViewModel(vm);

            adorner.Content = itemElement as FrameworkElement;
            adorner.DataContext = clusterViewModel;

            clusterViewModel.IsVisible = IsVisible;

            // Applying the visibility binding to the entire control rather than a grid within the control may benefit performance (when we turn things invisible)
            var visibilityBinding = new Binding("IsVisible") { Source = clusterViewModel, Mode = BindingMode.OneWay, Converter = VisibilityConverter };
            adorner.SetBinding(UIElement.VisibilityProperty, visibilityBinding);

            if (LabelTemplateSelector != null)
            {
                var t = LabelTemplateSelector.FindTemplate<DataTemplate>(vm.Data);

                if (t != null)
                {
                    FrameworkElement fe = MapHelper.CreateElement(t, vm.Data);
                    if (fe != null)
                    {
                        // 2 options for labels

                        // Option 1. Associate each label with the adorner.
                        //if (!UseLabelLayer)
                        //{
                        //    adorner.Overlay = fe;
                        //}
                        //else
                        {
                            // Option 2a: Add label to a dynamic label layer.
                            #region option 2a
                            // First, need an attached propery binding which cannot be specified without resorting to xaml
                            // Assuming we are adding to a dynamic layer. Try adding to a a static layer type called LabelLayer first

                            //string canvasLeftBindingDefinition = "<Binding xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" Path=\"(Canvas.LeftProperty)\" />";
                            //Binding canvasLeftBinding = XamlReader.Load(canvasLeftBindingDefinition) as Binding;
                            //canvasLeftBinding.Source = adorner;

                            //labelElement.SetBinding(Canvas.LeftProperty, canvasLeftBinding);

                            //string canvasTopBindingDefinition = "<Binding xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" Path=\"(Canvas.TopProperty)\" />";
                            //Binding canvasTopBinding = XamlReader.Load(canvasTopBindingDefinition) as Binding;
                            //canvasTopBinding.Source = adorner;

                            //labelElement.SetBinding(Canvas.TopProperty, canvasTopBinding);

                            #endregion

                            //option 2b. Add label to a static label layer.
                            #region option 2b


                            if (vm.Position != null)
                            {
                                var labelElement = new OverlayLabel { Overlay = fe };

                                var labelVisibilityBinding = new Binding("ShowDetails") { Source = clusterViewModel, Mode = BindingMode.OneWay, Converter = LabelVisibilityConverter };
                                labelElement.SetBinding(UIElement.VisibilityProperty, labelVisibilityBinding);
                                labelElement.DataContext = clusterViewModel;

                                labelLayerContents.Add(vm, labelElement);

                                Point offset = LabelLayer.LocationToCelPixel(vm.Position);
                                Canvas.SetLeft(labelElement, offset.X);
                                Canvas.SetTop(labelElement, offset.Y);

                                LabelLayer.Add(vm, labelElement);
                            }
                            #endregion
                        }
                    }
                }
            }

            AddElementToLayer(clusterViewModel, adorner);
        }

        // Decide which layer to add the element
        void AddElementToLayer(GeoItemViewModel vm, UIElement element)
        {
            // if the element is clicked set the viewmodel to selected
            if (!(element is ILayer))
            {
                // Using add handler because the element may have it's own click handler and we want to always capture this event
                element.AddHandler(UIElement.MouseLeftButtonDownEvent, new MouseButtonEventHandler((o, e) =>
                {
                    vm.IsSelected = true;
                    e.Handled = false;
                }), true);                    

#if !WINDOWS_PHONE

                // Create context menu
                var atomicParent = GeoItemViewModelHelper.FindAtomicFeatureRoot(vm);

                if (atomicParent == null)
                {
                    if (vm is ClusterViewModel)
                    {
                        var cvm = vm as ClusterViewModel;
                        atomicParent = cvm.WrappedViewModel;
                    }
                    else
                    {
                        atomicParent = vm;
                    }
                }

                // This is killing performance :( Can't add context menu to many vectors
                //if (atomicParent.Data is IMultiLocation)             

                var removable = atomicParent.Data as IRemovable;
                var multiLoc = atomicParent.Data as IMultiLocation;
                var locatable = atomicParent.Data as ILocatable;
                //var editable = atomicParent.Data as IEditable;

                //TODO: want to do this for drawn items only. This is not the best way to go about doing this.
                if (removable != null && removable.RemoveCommand != null)
                {
                    if (multiLoc != null)
                    {
                        Func<string> toStringFunc = () =>
                        {
                            LocationRectangle lr = multiLoc.GetBoundingBox();

                            const string refstr = "west, south, east, north";
                            string deg = string.Format("{0}, {1}, {2}, {3}", lr.West, lr.South, lr.East, lr.North);
                            string ddmmss = string.Format("{0}, {1}, {2}, {3}", Location.DDMMSSFromDecimal(lr.West), Location.DDMMSSFromDecimal(lr.South), Location.DDMMSSFromDecimal(lr.East), Location.DDMMSSFromDecimal(lr.North));
                            string sphericalMercator = string.Format("{0}, {1}, {2}, {3}", Location.SphericalMercatorFromDecimalLon(lr.West), Location.SphericalMercatorFromDecimalLat(lr.South), Location.SphericalMercatorFromDecimalLon(lr.East), Location.SphericalMercatorFromDecimalLat(lr.North));

                            return string.Format("{0}{4}{1}{4}{2}{4}{3}{4}", refstr, deg, ddmmss, sphericalMercator, Environment.NewLine);
                        };

                        ContextMenuHelper.AddClipboardCommand(element, "Copy bounding box to clipboard", toStringFunc);             
                    }
                    else if (locatable != null)
                    {          
                        Func<string> toStringFunc = () =>
                        {
                            Location loc = locatable.Position;
                            const string refstr = "Latitude, Longitude";
                            string deg = string.Format("{0}, {1}", loc.Latitude, loc.Longitude);
                            string ddmmss = string.Format("{0}, {1}", Location.DDMMSSFromDecimal(loc.Latitude), Location.DDMMSSFromDecimal(loc.Longitude));
                            string sphericalMercator = string.Format("{0}, {1}", Location.SphericalMercatorFromDecimalLat(loc.Latitude), Location.SphericalMercatorFromDecimalLon(loc.Longitude));

                            return string.Format("{0}{4}{1}{4}{2}{4}{3}{4}", refstr, deg, ddmmss, sphericalMercator, Environment.NewLine);
                        };

                        ContextMenuHelper.AddClipboardCommand(element, "Copy location to clipboard", toStringFunc);               
                    }


                    //if (editable != null)
                    //{
                    //    LambdaCommand initiateEditCommand = new LambdaCommand(() =>
                    //        {
                    //            editable.IsEditing = true;
                    //        });

                    //    ContextMenuHelper.AddEditMenuItem(element, initiateEditCommand);
                    //}

                    ICommand removeCommand = removable.RemoveCommand;

                    // removing a cluster removes all containing clustered items
                    if (vm is ClusterViewModel)
                    {
                        ClusterViewModel cvm = (ClusterViewModel)vm;

                        removeCommand = new LambdaCommand(() =>
                        {
                            foreach (ClusterViewModel child in cvm.GetClusteredItems())
                            {
                                var childRemovable = child.WrappedViewModel.Data as IRemovable;

                                if (childRemovable != null && childRemovable.RemoveCommand != null)
                                {
                                    childRemovable.RemoveCommand.Execute(null);
                                }
                            }
                            removable.RemoveCommand.Execute(null);
                        });
                    }

                    ContextMenuHelper.AddDeleteMenuItem(element, removeCommand, atomicParent);
                }
#endif
            }
            else
            {
                var layerVisibilityBinding = new Binding("IsActive") { Source = vm, Mode = BindingMode.TwoWay, Converter = new BooleanVisibilityConverter() };
                BindingOperations.SetBinding(element, UIElement.VisibilityProperty, layerVisibilityBinding);
            }

            // Element goes in Vector Layer
            if ((element is MapItemAdorner && ((MapItemAdorner)element).ContentIsSelfPositioning)
                 || ((!(element is MapItemAdorner) && element is ISelfPositioning)))
            {
                var path = element as ISelfPositioning;
                var adorner = element as MapItemAdorner;

                // This was for positioning labels
                //if (adorner != null)
                //{
                //    Point overlayPosition = VectorLayer.LocationToAbsolutePixel(vm.Position);
                //    adorner.OverlayPixelOffset = overlayPosition;
                //}

                VectorLayer.Add(vm, element);

                if (!vectorLayerContents.Keys.Contains(vm))
                {
                    vectorLayerContents.Add(vm, element);
                }
                else
                {
                    vectorLayerContents[vm] = element;
                }

            }
            else // Element goes in the Dynamic Layer
            {
                if (element is ISelfPositioning)
                {
                    var sp = element as ISelfPositioning;
                    sp.LocationToPoint = MapInstance.LocationToViewportPoint;
                    sp.Refresh(new Point(0,0), 16848);

                    var mia = element as MapItemAdorner;
                    if (mia.Content is ILocatable)
                    {
                        var locatable = mia.Content as ILocatable;

                        var pos = Layer.GetPosition(element);
                        if (pos == null)
                        {
                            Layer.SetPosition(element, locatable.Position);
                        }
                    }
                }
                
                DynamicLayer.Add(vm, element);

                GeoItemViewModel gvm = vm;
                if (vm is ClusterViewModel)
                {
                    lock (filterCollectionLock)
                    {
                        elementsToSynchronize.Add(vm as ClusterViewModel, element);
                    }
                    gvm = ((ClusterViewModel)vm).WrappedViewModel;
                }

                if (!dynamicLayerContents.ContainsKey(vm))
                {
                    dynamicLayerContents.Add(gvm, element);
                }
                else
                {
                    dynamicLayerContents[gvm] = element;
                }

                // TODO: Ensure this call does not break the application

                if (pendingAdds == null || pendingAdds.Count == 0)
                {
                    filterProcessor.TriggerProcessing(MapInstance.BoundingRectangle, MapInstance.CurrentViewportLogicalWidth, MapInstance.AsFrameworkElement.ActualWidth, elementsToSynchronize, dispatcher);
                }
            }
        }

        void Synchronize()
        {
            SynchronizeLayers();
            // the first time a layer is activated, if it is a load on demand layer, it may still be empty at this point and child elements will not be correctly synchronized.
            // Invoke the filters when elements are added
            if (elementsToSynchronize.Count > 0 && IsVisible)
            {
                this.DynamicLayer.SynchronizeOffsets();
                filterProcessor.TriggerProcessing(MapInstance.BoundingRectangle, MapInstance.CurrentViewportLogicalWidth, MapInstance.AsFrameworkElement.ActualWidth, elementsToSynchronize, dispatcher);
            }
        }

        void SynchronizeLayers()
        {
            if (viewModel != null && viewModel.IsActive)
            {
                if (RasterCrossoverViewportWidth <= MapInstance.View.ViewportWidth)
                {
                    if (RasterOverlay != null && IsVisible)
                    {
                        RasterOverlay.Visibility = Visibility.Visible;

                        if (UsingSharedLayers())
                        {
                            foreach (ClusterViewModel cvm in elementsToSynchronize.Keys)
                            {
                                cvm.IsVisible = false;
                            }

                            foreach (var kvp in vectorLayerContents)
                            {
                                ClusterViewModel cvm = kvp.Key as ClusterViewModel;
                                cvm.IsVisible = false;
                            }
                        }
                        else
                        {
                            DynamicLayer.Visibility = Visibility.Collapsed;
                            VectorLayer.Visibility = Visibility.Collapsed;
                        }

                        RasterOverlay.UseSprings = false;

                        if (RasterOverlay.ActualWidth != MapInstance.AsFrameworkElement.ActualWidth)
                        {
                            RasterOverlay.Width = MapInstance.AsFrameworkElement.ActualWidth;
                            RasterOverlay.Height = MapInstance.AsFrameworkElement.ActualHeight;
                        }

                        RasterOverlay.ViewportWidth = MapInstance.CurrentViewportLogicalWidth;

                        var centerGeographic = MapInstance.CurrentCenter;
                        var centerLogical = CoordinateTransformation.GeographicToLogical(centerGeographic);
                        var newOrigin = MapHelper.OriginFromLogicalCentreAtWidth(centerLogical, MapInstance, RasterOverlay);
                        RasterOverlay.ViewportOrigin = newOrigin;
                    }
                    if (RasterLabelOverlay != null && viewModel.ShowDetails)
                    {
                        RasterLabelOverlay.Visibility = Visibility.Visible;

                        if (RasterLabelOverlay.ActualWidth != MapInstance.AsFrameworkElement.ActualWidth)
                        {
                            RasterLabelOverlay.Width = MapInstance.AsFrameworkElement.ActualWidth;
                            RasterLabelOverlay.Height = MapInstance.AsFrameworkElement.ActualHeight;
                        }

                        RasterLabelOverlay.ViewportWidth = MapInstance.CurrentViewportLogicalWidth;

                        var centerGeographic = MapInstance.CurrentCenter;
                        var centerLogical = CoordinateTransformation.GeographicToLogical(centerGeographic);
                        var newOrigin = MapHelper.OriginFromLogicalCentreAtWidth(centerLogical, MapInstance, RasterLabelOverlay);
                        RasterLabelOverlay.ViewportOrigin = newOrigin;
                    }
                }
                else
                {
                    if (RasterOverlay != null && IsVisible)
                    {
                        RasterOverlay.Visibility = Visibility.Collapsed;

                        if (ViewModel != null && ViewModel.IsActive)
                        {
                            if (UsingSharedLayers())
                            {
                                foreach (var kvp in vectorLayerContents)
                                {
                                    ClusterViewModel cvm = kvp.Key as ClusterViewModel;
                                    cvm.IsVisible = true;
                                }
                            }
                            else
                            {
                                DynamicLayer.Visibility = Visibility.Visible;
                                VectorLayer.Visibility = Visibility.Visible;
                            }
                        }
                    }
                    if (RasterLabelOverlay != null && viewModel.ShowDetails)
                    {
                        RasterLabelOverlay.Visibility = Visibility.Collapsed;
                    }
                }
            }
        }

        // Visibility is not tied to the IsActive property as layers can be nested. (Visibility depends on parents too)
        // Only Static layers contents visibility is set directly. The Dynamic Layer elements and Raster Layer visibility is
        // set via the Synchronize method, as their visibility depends not only on Layer visibility. (For Dynamic Layer elements, their
        // visibility depends on the current map view)
        internal void OnViewModelActiveChanged()
        {
            if (IsVisible)
            {
                foreach (ClusterViewModel cvm in vectorLayerContents.Keys)
                {
                    cvm.IsVisible = true;
                }

                if (VectorLayer != null)
                {
                    VectorLayer.Refresh();
                }

                Synchronize();
            }
            else
            {
                if (RasterOverlay != null)
                {
                    RasterOverlay.Visibility = Visibility.Collapsed;
                }

                foreach (var cvm in elementsToSynchronize.Keys)
                {
                    cvm.IsVisible = false;
                }

                foreach (var kvp in vectorLayerContents)
                {
                    var cvm = (ClusterViewModel)kvp.Key;
                    cvm.IsVisible = false;
                }
            }

            foreach (LayerGroup l in ChildLayers)
            {
                l.OnViewModelActiveChanged();
            }

            SetLayerVisibility(IsVisible);
        }

        void OnViewModelShowDetailsChanged()
        {
            if (ViewModel.ShowDetails)
            {
                if (LabelLayer != null)
                {
                    LabelLayer.Refresh();
                }
            }

            var rasterable = viewModel.Data as IRasterable;
            if (rasterable!= null && rasterable.LabelsTileSource != null && RasterLabelOverlay == null)
                SynchronizeLayers();

            if (RasterLabelOverlay != null)
            {
                RasterLabelOverlay.Visibility = ViewModel.ShowDetails ? Visibility.Visible : Visibility.Collapsed;
                SynchronizeLayers();
            }
        }

        void SetLayerVisibility(bool visible)
        {
            Visibility visibility = visible ? Visibility.Visible : Visibility.Collapsed;

            if (!UsingSharedLayers())
            {
                VectorLayer.Visibility = visibility;
                DynamicLayer.Visibility = visibility;
                LabelLayer.Visibility = visibility;
            }
        }

        #region ZOrder and Opacity bindings
        void ResetChildLayerZOrdering()
        {
            // Take advantage of order of addition rules
            // Only do this if parentlayer is null - otherwise child layers should have the same z index as their parent
            // TODO: If users need to Z order child layers, then we need a better approach to setting Z index values.
            // i.e. do it in a depth first manner, passing an accumulator around (z index counter)
            // Doing the simplest thing for now. If using shared layers, then children have the same z index as the parent.
            if (ParentLayer == null && UsingSharedLayers())
            {
                int targetZ = ChildLayers.Count - 1;
                foreach (var sourceViewModel in ItemsSource)
                {
                    var corresponding = ChildLayers.Where(c => c.ViewModel == sourceViewModel).FirstOrDefault();

                    if (corresponding != null)
                    {
                        corresponding.ZOrder = targetZ;
                        targetZ--;
                    }
                }
            }

            if (!UsingSharedLayers())
            {
                // we can set z order in isolation
                int targetZ = ChildLayers.Count - 1;
                foreach (var sourceViewModel in ItemsSource)
                {
                    var corresponding = ChildLayers.Where(c => c.ViewModel == sourceViewModel).FirstOrDefault();

                    if (corresponding != null)
                    {
                        corresponding.ZOrder = targetZ;
                        targetZ--;
                    }
                }
            }
        }

        int zOrder;
        public int ZOrder
        {
            get { return zOrder; }
            set
            {
                if (zOrder != value)
                {
                    zOrder = value;
                    OnPropertyChanged("ZOrder");

                    if (UsingSharedLayers())
                    {
                        foreach (var child in ChildLayers)
                        {
                            child.ZOrder = value;
                        }
                    }
                }
            }
        }

        void ApplyZOrderBinding(FrameworkElement element)
        {
            var zOrderBinding = new Binding("ZOrder") { Source = this, Mode = BindingMode.OneWay };
            element.SetBinding(Canvas.ZIndexProperty, zOrderBinding);
        }

        double opacity = 1.0;
        public double Opacity
        {
            get { return opacity; }
            set
            {
                if (opacity != value)
                {
                    opacity = value;
                    OnPropertyChanged("Opacity");


                    // Let the underlying user object do this
                    //foreach (var child in ChildLayers)
                    //{
                    //    child.Opacity = value;
                    //}
                }
            }
        }

        void ApplyOpacityBinding(FrameworkElement element)
        {
            var opacityBinding = new Binding("Opacity") { Source = this, Mode = BindingMode.OneWay };
            element.SetBinding(FrameworkElement.OpacityProperty, opacityBinding);
        }

        void ApplyOpacityBinding(IItemsContainer itemsContainer)
        {
            this.Opacity = itemsContainer.Opacity;

            if (itemsContainer is INotifyPropertyChanged)
            {
                INotifyPropertyChanged inpc = itemsContainer as INotifyPropertyChanged;
                var weakEvent = new WeakEventListener<LayerGroup, INotifyPropertyChanged>(this, inpc)
                {
                    DetatchAction = (listener, source) => source.PropertyChanged -= listener.OnEvent,
                    EventAction = (self, sender, args) => { if (((PropertyChangedEventArgs)args).PropertyName == "Opacity") self.Opacity = ((IItemsContainer)sender).Opacity; }
                };
                inpc.PropertyChanged += weakEvent.OnEvent;
            }
        }

        void ApplyOpacityBinding(IRasterable rasterable)
        {
            INotifyPropertyChanged inpc = rasterable as INotifyPropertyChanged;
            var weakEvent = new WeakEventListener<LayerGroup, INotifyPropertyChanged>(this, inpc)
            {
                DetatchAction = (listener, source) => source.PropertyChanged -= listener.OnEvent,
                EventAction = (self, sender, args) => { if (((PropertyChangedEventArgs)args).PropertyName == "Opacity") self.Opacity = ((IItemsContainer)sender).Opacity; }
            };
            inpc.PropertyChanged += weakEvent.OnEvent;
        }
        #endregion

        #region ILayer
        public bool IsVisible
        {
            get { return (ViewModel == null || ViewModel.IsActive) && (ParentLayer == null || ParentLayer.IsVisible); }
        }

        public ILayer ParentLayer { get; set; }
        #endregion

        public event PropertyChangedEventHandler PropertyChanged;

        void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}
